------------Shadow Hawk One------------
A 4am crack                  2023-12-08
---------------------------------------

Name: Shadow Hawk One
Version: rev. 1
Genre: action
Year: 1981
Publisher: Horizon Simulations
Platform: Apple ][+ or later (48K)
Media: 5.25-inch disk
Sides: 1
OS: DOS 3.3
Previous cracks: none

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  no errors, but copy loads credits
  screen then clears memory and hangs

Locksmith Fast Disk Backup
  ditto

EDD 4 bit copy (no sync, no count)
  ditto

Copy ][+ nibble editor
  standard 16-sector disk structure
  nothing on track $23
  nothing on half/quarter tracks

Disk Fixer
  bootloader is standard
  track $11 even has a standard DOS 3.3
  disk catalog
  no obvious signs of shenanigans

I'm assuming there is some runtime
protection check, since computers
generally do not wipe memory and halt
unless someone tells them to.

                   ~

               Chapter 1
    In Which We Find A Little Extra


The game boots with a normal-looking,
normal-sounding DOS load followed by a
BASIC prompt, so let's try the simplest
thing that could possibly work: typing
<Ctrl-C> to break to BASIC.

; ...a well-timed <Ctrl-C> on boot...

BREAK
]LIST

 10  PRINT "BRUN OBJ.HELLO"

Well would you look at that.

]BLOAD OBJ.HELLO
]CALL-151

; DOS location of last BLOAD address
*AA72.AA73

AA72- 03 08

*803L

; reboot on reset
0803-   A9 00       LDA   #$00
0805-   8D F2 03    STA   $03F2
0808-   A9 C6       LDA   #$C6
080A-   8D F3 03    STA   $03F3
080D-   A9 00       LDA   #$00
080F-   8D F4 03    STA   $03F4

; show hi-res page 1
0812-   8D 50 C0    STA   $C050
0815-   8D 57 C0    STA   $C057
0818-   8D 52 C0    STA   $C052

; print <CR> followed by <Ctrl-D>,
; which tells me we're about to print
; a DOS command
081B-   A9 8D       LDA   #$8D
081D-   20 ED FD    JSR   $FDED
0820-   A9 84       LDA   #$84
0822-   20 ED FD    JSR   $FDED

; print a DOS command
0825-   A0 00       LDY   #$00
0827-   B9 B1 08    LDA   $08B1,Y
082A-   20 ED FD    JSR   $FDED
082D-   C9 8D       CMP   #$8D
082F-   F0 04       BEQ   $0835
0831-   C8          INY
0832-   4C 27 08    JMP   $0827
0835-   20 ED FD    JSR   $FDED

More on this in a moment.

; wait a fairly long time
0838-   A0 2A       LDY   #$2A
083A-   A9 FF       LDA   #$FF
083C-   20 A8 FC    JSR   $FCA8
083F-   88          DEY
0840-   C0 00       CPY   #$00
0842-   D0 F6       BNE   $083A

; call into code we just loaded
0844-   20 00 40    JSR   $4000

*FC58G N 400<8B1.8FFM
BLOAD HEAD.PICM@@@@@@@@@@@@@@@@@@@@@@@@

*BLOAD HEAD.PIC
*AA72.AA73

AA72- 00 20

*AA60.AA61

AA60- 50 21

Cute. We're loading graphics data
directly into the hi-res page ($2000
bytes at address $2000), but there's
$150 bytes of executable code after the
graphic data that gets loaded into
$4000. And we're calling that after you
stare at the credits page for a while.

(This, incidentally, is where my non-
working copy decided to wipe memory and
halt, so we're definitely close.)

*4000L

; get RWTS parameter table address and
; store it in $00/$01
4000-   20 26 41    JSR   $4126
4003-   84 00       STY   $00
4005-   85 01       STA   $01

; get slot/drive
4007-   A0 01       LDY   #$01
4009-   B1 00       LDA   ($00),Y
400B-   AA          TAX
400C-   20 22 41    JSR   $4122

*4122L

4122-   6C 43 41    JMP   ($4143)

*4143.4144

4143- 0D 41

*410DL

; decrypt $401A..$410B
410D-   A9 40       LDA   #$40
410F-   85 3D       STA   $3D
4111-   A9 19       LDA   #$19
4113-   85 3C       STA   $3C
4115-   A0 F2       LDY   #$F2
4117-   B1 3C       LDA   ($3C),Y
4119-   49 DD       EOR   #$DD
411B-   91 3C       STA   ($3C),Y
411D-   88          DEY
411E-   D0 F7       BNE   $4117
4120-   60          RTS

Continuing from $400F...

400F-   A9 03       LDA   #$03
4011-   8D 21 41    STA   $4121
4014-   20 1A 40    JSR   $401A

The decryption routine at $410D is
self-contained, so let's just run it.

*410DG

*401AL

401A-   60          RTS

What.

4017-   20 0D 41    JSR   $410D

What.

*410DG

*401AL

; turn on the drive motor and wait
401A-   BD 89 C0    LDA   $C089,X
401D-   8E 25 41    STX   $4125
4020-   A9 FF       LDA   #$FF
4022-   20 A8 FC    JSR   $FCA8
4025-   CE 21 41    DEC   $4121
4028-   10 F6       BPL   $4020

Wait, so we actually ENcrypted this,
then called it (which immediately
returned, because the first encrypted
byte was an "RTS" opcode), then we
DEcrypted it, and now we're... falling
through to execute the code as it was
in the first place.

I mean, sure. Why not.

                   ~

               Chapter 2
  In Which We Count Cycles, Which Is
  A Perfectly Reasonable Thing To Do
          On A Friday Evening
          Or Any Time, Really


Continuing from $402A. The drive motor
is on and we have waited for it to spin
up, and now we're going to do dastardly
things (probably).

; look for a real nibble
402A-   AE 25 41    LDX   $4125
402D-   A0 FF       LDY   #$FF
402F-   BD 8C C0    LDA   $C08C,X
4032-   30 05       BMI   $4039
4034-   88          DEY
4035-   D0 F8       BNE   $402F
4037-   F0 1A       BEQ   $4053

; initialize counters, probably
4039-   A9 00       LDA   #$00
403B-   8D 25 41    STA   $4125
403E-   A9 10       LDA   #$10
4040-   8D 29 41    STA   $4129
4043-   A9 04       LDA   #$04
4045-   8D 21 41    STA   $4121
4048-   78          SEI

; decrement death counters and
; continue at $4097, or fall through
; to The Badlands
4049-   CE 25 41    DEC   $4125
404C-   D0 49       BNE   $4097
404E-   CE 29 41    DEC   $4129
4051-   D0 44       BNE   $4097

*4097L

; look for $D5 nibble
4097-   BD 8C C0    LDA   $C08C,X
409A-   10 FB       BPL   $4097
409C-   C9 D5       CMP   #$D5
409E-   D0 A9       BNE   $4049

; skip a bunch of nibbles
40A0-   A0 0D       LDY   #$0D
40A2-   EA          NOP
40A3-   BD 8C C0    LDA   $C08C,X
40A6-   10 FB       BPL   $40A3
40A8-   88          DEY
40A9-   D0 F7       BNE   $40A2

; look for $EB nibble (would be part of
; the address field epilogue after
; skipping $0D nibbles)
40AB-   C9 EB       CMP   #$EB
40AD-   D0 9A       BNE   $4049
40AF-   F0 00       BEQ   $40B1

; note: no BPL loop here, we're just
; reading the data latch once
40B1-   BD 8C C0    LDA   $C08C,X
40B4-   C9 08       CMP   #$08
40B6-   B0 91       BCS   $4049

; check for non-standard $FD nibble
40B8-   BD 8C C0    LDA   $C08C,X
40BB-   10 FB       BPL   $40B8
40BD-   C9 FD       CMP   #$FD
40BF-   D0 88       BNE   $4049

; decrement counter (initially 4, set
; at $4045) and do the entire thing
; again
40C1-   CE 21 41    DEC   $4121
40C4-   D0 D1       BNE   $4097

; turn off the drive motor and exit to
; caller
40C6-   BD 88 C0    LDA   $C088,X
40C9-   58          CLI
40CA-   60          RTS

So we're looking for timing bits after
the third address field epilogue nibble
($EB), then a non-standard nibble after
that ($FD).

This is the heart of the protection.
But I want to dig in a bit further.
When we say "looking for timing bits,"
what is that exactly, and why does it
work as a protection?

On the original disk, there are two 0
bits after the $EB nibble in the
address field epilogue. Bit copiers can
not distinguish between one 0 bit and
two 0 bits after a nibble, because the
disk keeps spinning independently of
the CPU, which isn't fast enough to do
the necessary calculations in real
time. Bit copiers don't actually (ever)
make a faithful reproduction of the
original bitstream, because they can't.
They always take their best guess to
reconstruct the bitstream. This is an
unusual place to have two 0 bits in a
row, and bit copiers reconstruct it
incorrectly.

Here is the bitstream on the original
disk:

1110101100111111010011111101
|--EB--|  |--FD--|  |--FD--|

The data latch will hold its value
through the two 0 bits after the $EB
nibble, then it resets to #$00, then
new bits from the $FD nibble start
shifting in. At a rate of one bit every
four CPU cycles, the data latch will
reset somewhere around the time the CPU
is executing the instruction at address
$40AD.

Here's what's happening on the CPU
while the disk spins. (I've marked the
cycles required for each instruction.)

40A3-   BD 8C C0    LDA   $C08C,X   ; 4

[first 0 bit seen,
 data latch remains #$EB]

40A6-   10 FB       BPL   $40A3     ; 2
40A8-   88          DEY             ; 2

[second 0 bit seen,
 data latch remains #$EB]

40A9-   D0 F7       BNE   $40A2     ; 2
40AB-   C9 EB       CMP   #$EB      ; 2

[data latch resets to #$00,
 first 1 bit of $FD shifts in,
 data latch becomes #$01]

40AD-   D0 9A       BNE   $4049     ; 2
40AF-   F0 00       BEQ   $40B1     ; 3

[second 1 bit of $FD shifts in,
 data latch becomes #$03]

40B1-   BD 8C C0    LDA   $C08C,X

Most of the time (75%, as we will see
shortly), when we read the data latch
at $40B1, we'll get #$03. But there can
be some variation, because a bit gets
shifted into the data latch every four
CPU cycles, and that shift might have
happened 1-3 CPU cycles before the
fetch at $40A3. In the worst possible
case, by the time we check the data
latch at $40B1, we may have spent up to
12 CPU cycles:

  3 CPU cycle deficit
  2 not taking the BNE at $40AD
  3 taking the BEQ at $40AF
+ 4 on the LDA at $40B1
---
 12

This worst case will happen 25% of the
time, but it's still fine. In 12 CPU
cycles, only 3 bits will have shifted
into the data latch (because 12/4 = 3),
so its maximum value is 00000111 in
binary, which is #$07. Which means...

40B4-   C9 08       CMP   #$08
40B6-   B0 91       BCS   $4049

...it will never be #$08 or more, so
execution always falls through to the
success path at $40B8.

Now, on my non-working EDD bit copy,
the (reconstructed) bitstream looks
like this:

11101011011111101011111101
|--EB--| |--FD--| |--FD--|

Aha! EDD only added a single 0 bit
after the $EB nibble, instead of two 0
bits like the original disk. That means
the following bits will shift into the
data latch four cycles sooner than they
would on an original disk. It's still
possible for the data latch to end up
at #$07 when we need it to be less than
#$08 (at $40B1), but that's now the
*best* possible case and only happens
25% of the time.

The other 75% of the time, an
additional 1 bit will have shifted in
(compared to the original disk), the
value of the data latch at $40B1 will
be #$0F, the comparison at $40B4 will
fail, and the protection routine will
branch back and try again. If it fails
too often, the death counter will hit 0
and we'll fall through to The Badlands
at $4053:

*4053L

; turn off the drive motor
4053-   BD 88 C0    LDA   $C088,X

; set reset vector to $0002
4056-   A9 02       LDA   #$02
4058-   8D F0 03    STA   $03F0
405B-   8D F2 03    STA   $03F2
405E-   85 00       STA   $00
4060-   A9 00       LDA   #$00
4062-   8D F1 03    STA   $03F1
4065-   8D F3 03    STA   $03F3
4068-   85 01       STA   $01
406A-   49 A5       EOR   #$A5
406C-   8D F4 03    STA   $03F4

; print message on text screen, which
; is hidden
406F-   20 CB 40    JSR   $40CB

; copy the rest down to zero page and
; continue from there
4072-   A0 18       LDY   #$18
4074-   B9 7F 40    LDA   $407F,Y
4077-   91 00       STA   ($00),Y
4079-   88          DEY
407A-   10 F8       BPL   $4074
407C-   6C 00 00    JMP   ($0000)

; wipe all of main memory, $0800+
407F-   A9 00       LDA   #$00
4081-   85 00       STA   $00
4083-   A8          TAY
4084-   A9 08       LDA   #$08
4086-   85 01       STA   $01
4088-   A9 12       LDA   #$12
408A-   91 00       STA   ($00),Y
408C-   E6 00       INC   $00
408E-   D0 FA       BNE   $408A
4090-   E6 01       INC   $01
4092-   10 F6       BPL   $408A
4094-   58          CLI

; halt (only works on older machines)
4095-   02          ???
4096-   60          RTS

This is what I saw on my non-working
copy.

Fun(*) fact: if you press <Ctrl-Reset>
after it wipes memory, you can actually
see the message it prints on the text
screen:

           UNAUTHORIZED COPY
          PLEASE USE ORIGINAL

(*) not guaranteed, actual fun may vary

                   ~

               Chapter 3
        In Which We Just Say No
    And Our Story Comes To A Swift
       And Satisfying Conclusion


This protection routine has no side
effects. It either returns or it
doesn't. I can put an "RTS" at the
beginning and the caller will assume
everything went fine.

T12,S09,$04: 20 -> 60

But wait, there's more! For the low,
low price of one protection check, you
get another protection check absolutely
free!

There is another protection routine,
identical to this one, tacked onto the
end of the graphical title screen that
is loaded immediately after the credits
screen. Like, literally the same deal:
graphic data at $2000, but the file is
$150 bytes longer than it should be,
and the extra code at $4000 is the
protection check.

T05,S0F,$04: 20 -> 60

Quod erat liberandum.

---------------------------------------
A 4am crack                    No. 3204
------------------EOF------------------
